%{

// parser.y in goyacc format

// Copyright 2009 Jan Hosang <jan.hosang@gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.


package gbparser

import (
    "fmt"
    "os"
    "bufio"
    "utf8"
    "unicode"
    "container/list"
    
    "gb/recipe"
    "gb/invocation"
    "gb/tools"
)

type RuneStream struct {
    lineStream chan string
    runeStream chan int
    line string
    peek, prev int
    lineno int
}

var streamer *RuneStream

var newLine bool = true
var lastSym string

var recipes *list.List
var invocations *list.List
var variables map[string] *list.List
%}

%union
{
    sval string
    list *list.List
    invoc *invocation.Invocation
    recp *recipe.Recipe
}

%type <list>    strlist invocations osources mstring
%type <invoc>   invocation
%type <recp>    recipe
%type <sval>    string

%token <sval>   STRING
%token <list>   STRINGLIST
%token <>       ENDL

%%
gbfile:
    section | gbfile section
    
section:
    recipe              { recipes.PushBack((interface{})($1)) }
    | invocation ENDL   { invocations.PushBack((interface{})($1)) }
    | assignment | ospace ENDL
    
recipe:
    invocation invocations ENDL
    { $$ = recipe.New($1.Label, $1.Targets, $1.Sources, $2) }

strlist:
    ospace mstring                  { $$ = list.New(); ListExtend($$, $2) }
    | strlist ospace mstring        { $$ = $1;         ListExtend($$, $3) }
    
invocations:
    '\t' invocation                 { $$ = list.New(); $$.PushBack((interface{})($2)) }
    | invocations '\t' invocation   { $$ = $1;         $$.PushBack((interface{})($3)) }
    
invocation:
    string strlist osources ENDL    { $$ = invocation.New($1, $2, $3) }
    
assignment:
    string '=' strlist ENDL         { variables[$1] = $3 }
    
string:
    STRING                          { $$ = $1 }
    
mstring:
    string                          { $$ = list.New(); $$.PushBack((interface{})($1)) }
    | STRINGLIST                    { $$ = $1 }
    
// optional rules
ospace:
    | '\t'

osources:
                        { $$ = list.New() }
    | ':' strlist       { $$ = $2 }
%%

type GBLex int

func ParseBuildFile(file string) (*list.List, *list.List) {
    newLine = true
    recipes = list.New()
    invocations = list.New()
    variables = make(map[string] *list.List)
    
    streamer = NewRuneStream(file)
    
    yyParse(GBLex(0))
    
    return invocations, recipes
}

func yyError(s string) {
    Error("syntax error, last name: %v", lastSym)
}

func Error(s string, v ...interface{}) {
    fmt.Printf("Line %v: %v\n    ", streamer.Lineno(), streamer.Currline())
    fmt.Printf(s, v)
    fmt.Printf("\n")
}


func isNonAlpha(c int) bool {
    if unicode.IsLetter(c) || unicode.IsDigit(c) || c == '_' {
        return false
    }
    return true
}

func isRBrace(c int) bool {
    return c == ')'
}

func (l GBLex) Lex(yylval *yySymType) int {
    var endRune int
    var sym string
    symlist := list.New()
    
    c := streamer.Getc()
    
    tNewLine := newLine
    newLine = false
    
loop: // for skipping spaces
    if unicode.IsLetter(c) {
        goto alpha
    }
    switch(c) {
        case '\n':
            newLine = true
            return ENDL
        case '\r', ' ':
            c = streamer.Getc()
            goto loop
        case '\t':
            if tNewLine {
                return '\t'
            } else {
                c = streamer.Getc()
                goto loop
            }
        case '#':
            goto comment
        case ':', '=', 0:
            return c
        case '\'':
            goto singleQuoted
        case '"':
            goto doubleQuoted
    }
    if !unicode.IsSpace(c) {
        goto alpha
    }
    return c
    
alpha:
    sym = ""
    for !unicode.IsSpace(c) && c != ':' && c != '=' && c != 0 {
        if c == '$' {
            vlist := ParseVariable()
            if vlist != nil {
                for element := range vlist.Iter() {
                    sym += element.(string)
                    symlist.PushBack(sym)
                    sym = ""
                }
                back := symlist.Back()
                sym = back.Value.(string)
                symlist.Remove(back)
            }
        } else {
            sym += string(c)
        }
        c = streamer.Getc()
    }
    streamer.Ungetc(c)
    
stringListReturn:
    if symlist.Len() == 0 {
        yylval.sval = sym
        lastSym = sym
        return STRING
    } else {
        symlist.PushBack(sym)
        yylval.list = symlist
        lastSym = symlist.Back().Value.(string)
        return STRINGLIST
    }

doubleQuoted:
    endRune = c
    sym = ""
    c = streamer.Getc()
    for c != endRune && c != 0 {
        if c == '$' {
            vlist := ParseVariable()
            if vlist != nil {
                for element := range vlist.Iter() {
                    sym += element.(string)
                    symlist.PushBack(sym)
                    sym = ""
                }
                back := symlist.Back()
                sym = back.Value.(string)
                symlist.Remove(back)
            }
        } else {
            sym += string(c)
        }
        c = streamer.Getc()
    }
    
    goto stringListReturn
    
singleQuoted:
    endRune = c
    sym = ""
    c = streamer.Getc()
    for c != endRune && c != 0 {
        sym += string(c)
        c = streamer.Getc()
    }
    
    yylval.sval = sym
    lastSym = sym
    
    return STRING

comment: // #.*\n
    c = streamer.Getc()
    for c != '\n' && c != 0 {
        c = streamer.Getc()
    }
    goto loop
}

func ParseVariable() *list.List {
    c := streamer.Getc()
    braced := c == '('

    var isStopRune func(int) bool
    if braced {
        isStopRune = isRBrace
        c = streamer.Getc()
    } else {
        isStopRune = isNonAlpha
    }

    sym := ""
    for !isStopRune(c) {
        if c == 0 || c == '\n' {
            Error("line ends before variable name")
            if braced {
                sym = "$("+sym
            } else {
                sym = "$"+sym
            }
            l := list.New()
            l.PushBack((interface{})(sym))
            return l
        }
        sym += string(c)
        c = streamer.Getc()
    }

    if !braced { streamer.Ungetc(c) }
    
    value, ok := variables[sym]
    if !ok {
        l := list.New()
        sym = "$("+sym+")"
        l.PushBack((interface{})(sym))
        return l
    }
    return value
}

//////////////////////////////////////////////////////////////////////////////
// Helpers

func ListExtend(l *list.List, els *list.List) {
    for el := range els.Iter() {
        l.PushBack(el)
    }
}

//
//////////////////////////////////////////////////////////////////////////////


//////////////////////////////////////////////////////////////////////////////
// RuneStream

func NewRuneStream(filename string) (*RuneStream) {
    rs := new(RuneStream)
    
    rs.lineStream = make(chan string)
    rs.runeStream = make(chan int)
    go rs.streamLines(filename)
    go rs.streamRunes()
    
    rs.peek = -1
    rs.lineno = 0
    
    return rs
}

func (rs *RuneStream) streamLines(filename string) {
    f, err := os.Open(filename, os.O_RDONLY, 0);
    if err != nil {
        fmt.Printf("Fatal Error: %v\n", err);
        os.Exit(1);
    }
    fi := bufio.NewReader(f);

    s, err := fi.ReadString('\n')
    for err == nil {
        rs.lineStream <- s
        s, err = fi.ReadString('\n')
    }
    close(rs.lineStream)
    
    f.Close()
}

func (rs *RuneStream) streamRunes() {
    line, ok := <-rs.lineStream, !closed(rs.lineStream)
    for ok {
        rs.line = line
        linep := 0
        rs.lineno++
        
        for linep < len(line) {
            c, n := utf8.DecodeRuneInString(line[linep:len(line)])
            linep += n
            rs.runeStream <- c
        }
        line, ok = <-rs.lineStream, !closed(rs.lineStream)
    }
    close(rs.runeStream)
}

func (rs *RuneStream) Lineno() int {
    return rs.lineno
}

func (rs *RuneStream) Currline() string {
    i := len(rs.line)-1
    for i >= 0 && (rs.line[i] == '\n' || rs.line[i] == '\r') {
        i--
    }
    return rs.line[0:i+1]
}

func (rs *RuneStream) Getc() int {
    if rs.peek == -1 && closed(rs.runeStream) {
        return 0
    }
    
    if rs.peek != -1 {
        defer func(){rs.peek = -1}()
        return rs.peek
    }
    
    c, ok := <-rs.runeStream, !closed(rs.runeStream)
    if !ok {
        return 0
    }
    
    if c == '\\' {
        c, ok = <-rs.runeStream, !closed(rs.runeStream)
        for ok && c != '\n' {
            c, ok = <-rs.runeStream, !closed(rs.runeStream)
        }
        if ok {
            // get that '\n' out of the way
            c, ok = <-rs.runeStream, !closed(rs.runeStream)
        }
    }
    if !ok {
        return 0
    }
    return c
}

func (rs *RuneStream) Ungetc(c int) {
    if rs.peek != -1 {
        panic("You cannot RuneStream.Ungetc() twice without Getc() in between.")
    } else {
        rs.peek = c
    }
}

// RuneStream
//////////////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////////////
// ToString() functions

func allToString() string {
    return tools.ListToStringf(invocations, "", "\n", "\n") + tools.ListToStringf(recipes, "", "", "")
}

